#pragma once

#include <memory>
#include <string>
#include <utility>

#include "JsiSkHostObjects.h"
#include "JsiSkImageInfo.h"
#include "JsiSkMatrix.h"
#include "JsiSkShader.h"
#include "third_party/base64.h"

#include "RNSkTypedArray.h"

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"

#include "include/codec/SkEncodedImageFormat.h"
#include "include/core/SkImage.h"
#include "include/core/SkStream.h"
#include "include/encode/SkJpegEncoder.h"
#include "include/encode/SkPngEncoder.h"
#include "include/encode/SkWebpEncoder.h"

#pragma clang diagnostic pop

#include <jsi/jsi.h>

namespace RNSkia {
namespace jsi = facebook::jsi;

class JsiSkImage : public JsiSkWrappingSkPtrHostObject<SkImage> {
public:
    // TODO-API: Properties?
    JSI_HOST_FUNCTION(width)
    {
        return static_cast<double>(getObject()->width());
    }
    JSI_HOST_FUNCTION(height)
    {
        return static_cast<double>(getObject()->height());
    }

    JSI_HOST_FUNCTION(getImageInfo)
    {
        return JsiSkImageInfo::toValue(runtime, getContext(), getObject()->imageInfo());
    }

    JSI_HOST_FUNCTION(makeShaderOptions)
    {
        auto tmx = (SkTileMode)arguments[0].asNumber();
        auto tmy = (SkTileMode)arguments[1].asNumber();
        auto fm = (SkFilterMode)arguments[2].asNumber();
        auto mm = (SkMipmapMode)arguments[3].asNumber();
        auto m =
            count > 4 && !arguments[4].isUndefined() ? JsiSkMatrix::fromValue(runtime, arguments[4]).get() : nullptr;
        auto shader = getObject()->makeShader(tmx, tmy, SkSamplingOptions(fm, mm), m);
        return jsi::Object::createFromHostObject(runtime,
            std::make_shared<JsiSkShader>(getContext(), std::move(shader)));
    }

    JSI_HOST_FUNCTION(makeShaderCubic)
    {
        auto tmx = (SkTileMode)arguments[0].asNumber();
        auto tmy = (SkTileMode)arguments[1].asNumber();
        auto B = SkDoubleToScalar(arguments[2].asNumber());
        auto C = SkDoubleToScalar(arguments[3].asNumber());
        auto m =
            count > 4 && !arguments[4].isUndefined() ? JsiSkMatrix::fromValue(runtime, arguments[4]).get() : nullptr;
        auto shader = getObject()->makeShader(tmx, tmy, SkSamplingOptions({ B, C }), m);
        return jsi::Object::createFromHostObject(runtime,
            std::make_shared<JsiSkShader>(getContext(), std::move(shader)));
    }

    sk_sp<SkData> encodeImageData(const jsi::Value *arguments, size_t count)
    {
        // Get optional parameters
        auto format =
            count >= 1 ? static_cast<SkEncodedImageFormat>(arguments[0].asNumber()) : SkEncodedImageFormat::kPNG;

        auto quality = (count >= 2 && arguments[1].isNumber()) ? arguments[1].asNumber() : 100.0;
        auto image = getObject();
        if (image->isTextureBacked()) {
            image = image->makeNonTextureImage();
        }
        sk_sp<SkData> data;

        if (format == SkEncodedImageFormat::kJPEG) {
            SkJpegEncoder::Options options;
            options.fQuality = quality;
            data = SkJpegEncoder::Encode(nullptr, image.get(), options);
        } else if (format == SkEncodedImageFormat::kWEBP) {
            SkWebpEncoder::Options options;
            if (quality >= 100) {
                options.fCompression = SkWebpEncoder::Compression::kLossless;
                options.fQuality = 75; // This is effort to compress
            } else {
                options.fCompression = SkWebpEncoder::Compression::kLossy;
                options.fQuality = quality;
            }
            data = SkWebpEncoder::Encode(nullptr, image.get(), options);
        } else {
            SkPngEncoder::Options options;
            data = SkPngEncoder::Encode(nullptr, image.get(), options);
        }

        return data;
    }

    JSI_HOST_FUNCTION(encodeToBytes)
    {
        auto data = encodeImageData(arguments, count);

        auto arrayCtor = runtime.global().getPropertyAsFunction(runtime, "Uint8Array");
        size_t size = data->size();

        jsi::Object array = arrayCtor.callAsConstructor(runtime, static_cast<double>(size)).getObject(runtime);
        jsi::ArrayBuffer buffer = array.getProperty(runtime, jsi::PropNameID::forAscii(runtime, "buffer"))
                                      .asObject(runtime)
                                      .getArrayBuffer(runtime);

        auto bfrPtr = reinterpret_cast<uint8_t *>(buffer.data(runtime));
        memcpy(bfrPtr, data->bytes(), size);
        return array;
    }

    JSI_HOST_FUNCTION(encodeToBase64)
    {
        auto data = encodeImageData(arguments, count);

        auto len = Base64::Encode(data->bytes(), data->size(), nullptr);
        auto buffer = std::string(len, 0);
        Base64::Encode(data->bytes(), data->size(), reinterpret_cast<void *>(&buffer[0]));
        return jsi::String::createFromAscii(runtime, buffer);
    }

    JSI_HOST_FUNCTION(readPixels)
    {
        int srcX = 0;
        int srcY = 0;
        if (count > 0 && !arguments[0].isUndefined()) {
            srcX = static_cast<int>(arguments[0].asNumber());
        }
        if (count > 1 && !arguments[1].isUndefined()) {
            srcY = static_cast<int>(arguments[1].asNumber());
        }
        SkImageInfo info = (count > 2 && !arguments[2].isUndefined()) ?
            *JsiSkImageInfo::fromValue(runtime, arguments[2]) :
            SkImageInfo::MakeN32(getObject()->width(), getObject()->height(), getObject()->imageInfo().alphaType());
        size_t bytesPerRow = 0;
        if (count > 4 && !arguments[4].isUndefined()) {
            bytesPerRow = static_cast<size_t>(arguments[4].asNumber());
        } else {
            bytesPerRow = info.minRowBytes();
        }
        auto dest = count > 3 ? RNSkTypedArray::getTypedArray(runtime, arguments[3], info) :
                                RNSkTypedArray::getTypedArray(runtime, jsi::Value::null(), info);
        if (!dest.isObject()) {
            return jsi::Value::null();
        }
        jsi::ArrayBuffer buffer = dest.asObject(runtime)
                                      .getProperty(runtime, jsi::PropNameID::forAscii(runtime, "buffer"))
                                      .asObject(runtime)
                                      .getArrayBuffer(runtime);
        auto bfrPtr = reinterpret_cast<void *>(buffer.data(runtime));

        if (!getObject()->readPixels(info, bfrPtr, bytesPerRow, srcX, srcY)) {
            return jsi::Value::null();
        }
        return dest;
    }

    JSI_HOST_FUNCTION(makeNonTextureImage)
    {
        auto image = getObject()->makeNonTextureImage();
        return jsi::Object::createFromHostObject(runtime, std::make_shared<JsiSkImage>(getContext(), std::move(image)));
    }

    EXPORT_JSI_API_TYPENAME(JsiSkImage, Image)

    JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkImage, width), JSI_EXPORT_FUNC(JsiSkImage, height),
        JSI_EXPORT_FUNC(JsiSkImage, getImageInfo), JSI_EXPORT_FUNC(JsiSkImage, makeShaderOptions),
        JSI_EXPORT_FUNC(JsiSkImage, makeShaderCubic), JSI_EXPORT_FUNC(JsiSkImage, encodeToBytes),
        JSI_EXPORT_FUNC(JsiSkImage, encodeToBase64), JSI_EXPORT_FUNC(JsiSkImage, readPixels),
        JSI_EXPORT_FUNC(JsiSkImage, makeNonTextureImage), JSI_EXPORT_FUNC(JsiSkImage, dispose))

    JsiSkImage(std::shared_ptr<RNSkPlatformContext> context, const sk_sp<SkImage> image)
        : JsiSkWrappingSkPtrHostObject<SkImage>(std::move(context), std::move(image))
    {}
};
} // namespace RNSkia
